#include <getopt.h>
#include <sys/types.h>
#include <md5.h>
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/queue.h>

#include <pkg.h>
#include <pkg_freebsd.h>

typedef enum {
        MATCH_ALL,
        MATCH_EXACT,
        MATCH_EREGEX,
        MATCH_GLOB,
        MATCH_NGLOB,
        MATCH_REGEX
} match_t;

int level = 0;

struct delete_config {
        STAILQ_HEAD(target_head, pkg_target) targets;
        int clear_directories;
        int fake;
        int force;
        int interactive;
        int no_deinstall;
        int recursive;
        int verbose;
        match_t match;
        const char *prefix;
        const char *database_location;
};

struct pkg_target {
        char target[PATH_MAX];
        STAILQ_ENTRY(pkg_target) next;
};

static void clean_up(int);
static void usage(void);
static int pkg_delete(struct delete_config *);
static int delete(struct pkg *p, struct pkg_database *, struct delete_config *);
static int pkg_info_hash_match(struct pkg_file *);
static void parse_arguments(int, char **);
static int pattern_match(match_t, const char *, const char *);
static int regex_match(const char *, const char *, int);
static int glob_match(const char *, const char *);
        
struct delete_config c;

#define SKIP_ALL 0
#define SKIP_REQUIRED_BY 0

static void
parse_arguments(int argc, char **argv)
{
        struct pkg_target *pt;

        static char opts[] = "adDfGhinp:rvxX";
        static struct option longopts[] = {
                { "all",	no_argument,		NULL,		'a' },
                { "clean-dirs",	no_argument,		NULL,		'd' },
                { "dry-run",	no_argument,		NULL,		'n' },
                { "extended",	no_argument,		NULL,		'X' },
                { "force",	no_argument,		NULL,		'f' },
                { "help",	no_argument,		NULL,		'h' },
                { "interactive",no_argument,		NULL,		'i' },
                { "prefix",	required_argument,	NULL,		'p' },
                { "recursive",	no_argument,		NULL,		'r' },
                { "regex",	no_argument,		NULL,		'x' },
                { "no-glob",	no_argument,		NULL,		'G' },
                { "no-script",	no_argument,		NULL,		'D' },
                { "no-scripts",	no_argument,		NULL,		'D' },
                { "verbose",	no_argument,		NULL,		'v' },
                { NULL,		0,			NULL,		0 },
        };

        int o;

        while ((o = getopt_long(argc, argv, opts, longopts, NULL)) != -1) {
                switch (o) {
                case ('v'):
                        c.verbose = 1;
                        break;
                case ('f'):
                        c.force = 1;
                        break;
                case ('p'):
                        c.prefix = optarg;
                        break;
                case ('D'):
                        c.no_deinstall = 1;
                        break;
                case ('d'):
                        c.clear_directories = 1;
                        break;
                case ('n'):
                        c.fake = 1;
                        c.verbose = 1;
                        break;
                case ('a'):
                        c.match = MATCH_ALL;
                        break;
                case ('G'):
                        c.match = MATCH_EXACT;
                        break;
                case ('x'):
                        c.match = MATCH_REGEX;
                        break;
                case ('X'):
                        c.match = MATCH_EREGEX;
                        break;
                case ('i'):
                        c.interactive = 1;
                        break;
                case ('r'):
                        c.recursive = 1;
                        break;
                case ('h'):
                default:
                        usage();
                        break;
                }
        }

        argv += optind;
        for (; *argv != NULL; ++argv) {
                pt = calloc(1, sizeof(*pt));
                strcpy(pt->target, *argv);
                STAILQ_INSERT_TAIL(&c.targets, pt, next);
        }
}

int
main(int argc, char **argv)
{
        int r;

        if (argc == 1) {
                usage();
        }

        STAILQ_INIT(&c.targets);
        c.prefix = "";
        c.match = MATCH_GLOB;
        parse_arguments(argc, argv);
        if (c.match != MATCH_ALL && STAILQ_EMPTY(&c.targets)) {
                warnx("missing package names(s)");
                usage();
        }

        r = pkg_delete(&c);
        clean_up(r);

        return (0);
}

static void
usage(void)
{
        clean_up(1);
}

static int
pkg_delete(struct delete_config *_c)
{
        struct pkg *p;
        struct pkg_database *db;
        struct pkg_target *pt;
        struct stat sb;
        int failure;
        int match_count;

        p = pkg_freebsd_create();
        db = pkg_freebsd_database_create();

        _c->database_location = getenv("PKG_DBDIR");
        if (_c->database_location == NULL) {
                _c->database_location = "/var/db/pkg";
        }

#if 0
        /* Make sure we can act on the database. */
        /* XXX: I should actually roll this the database code for libpkg. */
        stat(_c->database_location, &sb);
        if (!_c->fake ) {
                if (geteuid() != sb.st_uid) {
                        errx(1, "you do not own %s", _c->database_location);
                }
        }
#endif
        (void)sb;

        pkg_database_open(db, _c->database_location, 0);
        failure = 0;
        match_count = 0;

        if (_c->match == MATCH_ALL) {
                while (pkg_database_get_next_pkg(db, p, 0) == PKG_OK) {
                        if (delete(p, db, _c) != 0) {
                                failure++;
                        }
                        pkg_database_rewind(db);
                }
        } else {
                STAILQ_FOREACH(pt, &_c->targets, next) {
                        while (pkg_database_get_next_pkg(db, p, 0) == PKG_OK) {
                                if (pattern_match(_c->match,
                                        pkg_freebsd_get_name(p),
                                        pt->target)) {
                                        if (delete(p, db, _c) != 0) {
                                                failure++;
                                        }
                                        break;
                                }
                        }
                        pkg_database_rewind(db);
                }
        }
       
        return (failure);
}

static int
delete(struct pkg *p, struct pkg_database *db, struct delete_config *_c)
{
        struct pkg_dependency d;
        struct pkg_exec e;
        struct pkg_file f;
        struct pkg *pp;
        const char *script;
        const char *rb;
        char pathname[PATH_MAX];
        int rval;
        int r;
        int rb_count;
        const char *dep_name;
        const char *dep_origin;
        const char *pkg_name;
        
        /* We're assuming p is installed. */
        
        /* A second pkg to play with. */
        pp = pkg_freebsd_create();
        if (pp == NULL) {
                exit(99);
        }

        rval = 0;
        pkg_name = pkg_freebsd_get_name(p);

        /* 
         * Check the dependents for this package.  If we're doing a recursive
         * removal, do it here. 
         */
        rb_count = 0; /* This is for the second dependent check. */
        while ((rb = pkg_freebsd_get_next_required_by(p)) != NULL) {
                /* 
                 * Have to rewind since delete uses the cursor, and we got here
                 * from pkg_delete(), which is iterating through the database
                 * itself.
                 */
                ++rb_count;
                if (_c->recursive) {
                        pkg_database_rewind(db);
                        while (pkg_database_get_next_pkg(db, pp, 0) == PKG_OK) {
                                if (strcmp(rb, pkg_freebsd_get_name(pp)) == 0) {
                                        /* XXX: Apparently there's only supposed
                                         * to be one level of recursion... */
                                        delete(pp, db, _c);
                                        --rb_count;
                                }
                        }
                }
        }

        if (_c->interactive) {
                fprintf(stderr, "Delete %s?", pkg_name);
                fflush(stderr);
                /* XXX: */
        }
        
        if (rb_count > 0) {
                warnx("Package '%s' is required by these other packages and "
                        "may not be deinstalled %s:", pkg_name,
                        (_c->force ? "(but it will be deleted anyway)" : ""));
                pkg_freebsd_required_by_rewind(p);
                while ((rb = pkg_freebsd_get_next_required_by(p)) != NULL) {
                        pkg_database_rewind(db);
                        /* XXX: It would be nice to verify against the database
                         * here. */
                        fprintf(stderr, "%s\n", rb);
                }
        }

        script = pkg_freebsd_get_require_script_path(p);
        if (script != NULL) {
                if (_c->verbose) {
                        printf("Executing 'require' script.\n");
                }
                warnx("RUN: %s", script);
        }

        
        /*
         * XXX: I don't check to see if the old style post-deinstall script is
         * here.  I should add a check for that, or figure out a way to have
         * pkg_freebsd be smart enough to deal with it.
         */
        script = pkg_freebsd_get_deinstall_script_path(p);
        if (script != NULL) {
                if (_c->fake) {
                        printf("Would execute deinstall script at this "
                        "point.\n");
                } else {
                        warnx("RUN: %s %s %s", script, pkg_name, "DEINSTALL");
                }
        }

        while (pkg_freebsd_get_next_dependency(p, &d) == PKG_OK) {
                dep_name = pkg_freebsd_dependency_get_name(&d);
                if (_c->verbose) {
                        dep_origin = pkg_freebsd_dependency_get_origin(&d);
                        printf("Trying to remove dependency on package '%s'",
                                dep_name);
                        if (dep_origin != NULL) {
                                printf(" with '%s' origin", dep_origin);
                        }
                        printf(".\n");
                }
                if (_c->fake) {
                        continue;
                }
                /* 
                 * TODO: All these rewinds are getting tedious.  I should just
                 * add a get function to the database API.
                 */
                pkg_database_rewind(db);
                while (pkg_database_get_next_pkg(db, pp, 
                        SKIP_ALL ^ SKIP_REQUIRED_BY) == PKG_OK) {
                        if (strcmp(dep_name, pkg_freebsd_get_name(pp)) == 0) {
                                pkg_freebsd_remove_required_by(pp, pkg_name);
                                /* XXX: Uncomment this add. */
#if 0
                                r = pkg_database_add_pkg(db, pp, 
                                        SKIP_ALL ^ SKIP_REQUIRED_BY);
#endif
                                warnx("ADD: '%s' unrequired-by '%s'", dep_name,
                                        pkg_name);
                                r = PKG_OK;
                                if (r != PKG_OK) {
                                        warnx("Error removing required-by entry"
                                        "'%s' in package '%s'", pkg_name,
                                        pkg_freebsd_get_name(pp));
                                }
                        }
                }
        }

        while (pkg_freebsd_get_next_file(p, &f) == PKG_OK) {
                if (_c->prefix == NULL && 
                        pkg_freebsd_file_get_prefix(&f) == NULL) {
                        /* 
                         * We have no prefix.  Because we don't check for this
                         * ahead of time, we're sort of SOL.  Just die here and
                         * let the user clean up the mess.
                         */
                        errx(1, "File %s has no prefix!\n",
                                pkg_freebsd_file_get_pathname(&f));
                }
                /* Do a hash check for the file before we spend time building
                 * the path for it. */
                if (!pkg_info_hash_match(&f) ) {
                        warnx("'%s' fails original MD5 checksum - %s",
                        pkg_freebsd_file_get_pathname(&f),
                        (_c->force ? "deleted anyway." : "not deleted."));
                        if (!_c->force) {
                                /* 
                                 * The current pkg_delete tool continues in its
                                 * loop in this situation.  I don't like that,
                                 * because it will end up deleting the manifest
                                 * from the database while a file is still on
                                 * disk.
                                 */
                                return (1);
                        }
                }
                /* Now build a pathname for the file. */
                /* _c->prefix overrides the prefix from the packing list. */
                strcpy(pathname, 
                        (_c->prefix == NULL ? _c->prefix : 
                        pkg_freebsd_file_get_prefix(&f)));
                strcat(pathname, "/");
                strcat(pathname, pkg_freebsd_file_get_pathname(&f));

                if (_c->verbose) {
                        printf("Delete file %s\n", pathname);
                }
                if (!_c->fake) {
                        warnx("DELETE: %s", pathname);
                }
                if (pkg_freebsd_get_preserve(p)) {
                        /* Do preserve stuff... */
                }
        }

        while (pkg_freebsd_get_next_unexec(p, &e) == PKG_OK) {
                /* 
                 * I need to do whatever format_cmd does in here so that the
                 * command is run safely.
                 */
                if (_c->verbose) {
                        printf("Execute '%s'\n",
                                pkg_freebsd_exec_get_command(&e));
                }
                if (!_c->fake) {
#if 0
                        r = system(ue);
#endif
                        warnx("UNEXEC: %s",
                                pkg_freebsd_exec_get_command(&e));
#if 0
                        if (r != 0) {
                                warnx("unexec command for '%s' failed",
                                        pkg_freebsd_exec_get_command(&e));
                                /* 
                                 * Unlike the hash mismatch, not dying here
                                 * doesn't seem like a problem to me, so we'll
                                 * just keep on truckin'
                                 */
                                rval = 1;
                        }
#endif
                }
        }

        script = pkg_freebsd_get_post_deinstall_script_path(p);
        if (script != NULL) {
                if (_c->fake) {
                        printf("Would execute post-deinstall script at this "
                        "point.\n");
                } else {
                        warnx("RUN: %s %s %s", script, pkg_name, "DEINSTALL");
                }
        }

        /* Remove the package from the database. */
        pkg_database_remove_pkg(db, pkg_name);
        
        return (rval);
}

static void
clean_up(int r)
{
        exit(r);
}

static int
pkg_info_hash_match(struct pkg_file *f)
{
        char path[PATH_MAX];
        char hash[33];
        char *calc_hash;
        size_t link_len;
        char link_buf[PATH_MAX];
        struct stat sb;
        
        snprintf(path, PATH_MAX, "%s/%s",
                pkg_freebsd_file_get_prefix(f),
                pkg_freebsd_file_get_pathname(f));
        if (lstat(path, &sb) < 0) {
                /* 
                 * Technically, we're better than the current pkg_info here.
                 * pkg_info reports that a file doesn't exist just because it
                 * can't open it.  That's a bug.
                 */
                warnx("%s does not exist", path);
                return (0);
        }

        /* 
         * We're not dealing with pre 1.1 properties.  Symlink 
         * hash for a symlink is on the string returned by
         * readlink(). 
         */
        calc_hash = NULL;
        if (S_ISLNK(sb.st_mode)) {
                link_len = readlink(path, link_buf, PATH_MAX);
                if (link_len > 0) {
                        calc_hash = MD5Data(link_buf, link_len, hash);
                }
        } else if (S_ISREG(sb.st_mode)) {
                calc_hash = MD5File(path, hash);
        }
        if (calc_hash != NULL) {
                if (strcmp(calc_hash, pkg_freebsd_file_get_hash(f)) != 0) {
                        return (0);
                } else {
                        return (1);
                }
        }

        return (0);
}

static int
regex_match(const char *string, const char *pattern, int extended)
{
        int r;
        int be;
        regex_t re;
        
        be = (extended ? REG_EXTENDED : REG_BASIC);
        r = regcomp(&re, pattern, be | REG_NOSUB);
        if (r == 0) {
                r = regexec(&re, string, (size_t)0, NULL, 0);
                regfree(&re);
        }

        if (r == 0) {
                return (1);
        } else if (r != REG_NOMATCH) {
                /* XXX: Should print an error. */
                return (0);
        }

        return (0);
}

static int
glob_match(const char *string, const char *pattern)
{
        /* XXX: For now, do this just to get by. */
        return (strcmp(string, pattern) == 0);
}

static int
pattern_match(match_t match, const char *string, const char *pattern)
{
        /* XXX: I don't have version comparison in here yet. */

        switch (match) {
        case (MATCH_ALL):
                return (1);
        case (MATCH_NGLOB):
        case (MATCH_GLOB):
                return (glob_match(string, pattern));
        case (MATCH_EREGEX):
        case (MATCH_REGEX):
                return (regex_match(string, pattern, match == MATCH_EREGEX));
        default:
                break;
        }

        return (0);
}
